首页 »
知识库 »
Harness Engineering: C 端 AIGC 内容生产自优化实践
Harness Engineering: C 端 AIGC 内容生产自优化实践
来源:htmlDecode("阿里云开发者")
原文链接:https://mp.weixin.qq.com/s/2-bum4w_6xWAnc82fvk-3A
阿里妹导读
文章内容基于作者个人技术实践与独立思考,旨在分享经验,仅代表个人观点。
在前一篇 《Harness Engineering:为 AI 打造可持续迭代环境的实践》 中,我们讲了 HelixVerify 如何在线下环境用 114 次迭代 把风险样本召回率从 8% 提升到 98.86%。那是一个典型的 线下 Harness 。这一篇讲 Harness 思想搬到 C 端 AIGC 生产链路后的形态 —— 蚂蚁保保险快查深度解读页面生成系统 (Deep Interpretation Page Generator,以下简称 DIPG)。
DIPG 不让 C 端用户直接吃 LLM 实时生成的结果 ,而是把架构翻转成 "host-generate-verify-modify → DB 按品开启 → C 端直出" 。离线生成由一个带 verify 闭环的 Agentic Loop 负责,只有通过 verify 的 HTML 才会刷入 DB 并暴露给用户。实时生成只保留作为未开启品的兜底路径。即通过 Harness 的方式让 对 C 端交付的HTML 有足够好的质量。
** 为什么不能让实时生成直出? **
最开始当然试过用户点开详情页时调 LLM 实时生成 HTML 直出。不过问题也很明显:
时延扛不住 :一次完整的深度解读需要 agentic 检索素材与条款、 生成几千字 HTML,LLM 推理加起来几十秒。C 端用户等不起,"秒出"是基础体验要求。
质量扛不住 :LLM 生成 HTML 会出两类错 —— 渲染类错误 (孤儿闭合标签、组件层级错乱)让页面直接塌掉; 幻觉类错误 (数据不符、编造对比)让用户读到错信息。LLM 一次过做不到 100% 正确,直出就是赌。
C 端 AIGC 交付的本质要求是: 用户点开那一刻看到的 HTML,必须是已经被校验过的 。所以 DIPG 把实时生成降为兜底、把离线 Harness 的生产过程作为主路径。这篇文章就讲这个离线 verify 闭环在工程上怎么搭。
一、DIPG 的两条线上链路
DIPG 对外的产品形态:用户在支付宝保险快查里打开某款产品的详情页,页面中相应模块会出现一个"深度解读"模块 —— 那里渲染的就是 DIPG 产出的 HTML 片段(750 宽度移动端容器)。前端向后端带着 prodNo 发请求,后端返回 HTML 片段。
在线上生产环境里,这个系统同时跑着两条链路 —— 离线链路是主路径,负责交付质量;实时链路是兜底 :
两条链路的 Research Agent 完全同源 —— 离线链路在它之上套了一层 Host Agent(负责调度 Research/Verify、并自己按 verify 反馈 patch HTML);实时链路只跑一次 Research Agent, Host 和 Verify 都不参与 ,产出的 HTML 不经过后置校验也不会被修正。
用户读的都是离线产物 。DIPG 当前采用"离线刷入 DB + 按品维度开启"的方案:后台批量预生成并刷入 DB,只对"已开启的品"向 C 端暴露 —— 用户请求时直接从 DB 读离线产物, 命中率 100% ,不依赖缓存层兜底(线上还会叠加缓存做进一步加速,但那是性能优化,不是可用性前提)。实时链路仅作为 未开启品的兜底生成通道 存在,默认情况下 C 端看到的永远是离线产物。
离线链路--质量可控。如果实时生成能 100% 不出错,我们根本不需要离线。离线的核心价值是: 把 verify 闭环的修正机会还回来 ,让 Harness 有施展空间。
"合格 HTML 送达用户 :DIPG 的外层 Graph 末端有一个 callback 节点,它把 HTML + verify 结论 + error_code 一起通过 RPC 回调发给下游(insexpert 的 deepResearchCallBack ); 下游根据 error_code 决定是否把这份 HTML 刷入 DB (通过 verify 的才刷)。
** DIPG 内部的三个 Agent **
离线链路的"带 verify 闭环"不是一个魔法盒子 —— 它内部由 三个分工明确的 Agent 协作完成。这是后面所有工程讨论的概念起点:
Host Agent —— 总编排 + 精准修正。它读到用户请求后,按"研究 → 校验 → (若未通过)修正 → 再校验"的流程派活。当 Verify Agent 返回修正意见时, Host Agent 自己在已有 HTML 上做精准编辑 (按 fix_hint 定位段落、patch 掉问题点),而不是再派一次 Research Agent 去重新生成 —— 这是 DIPG 的一个关键设计,下面 5.4 节展开。
Research Agent —— 只负责 从零生成 。它拿到产品编号后下载素材、多轮读取条款、必要时搜网络,最后产出整份 HTML 片段。它 不参与修正循环 —— 修正不是它的工作,Host Agent 不会拿"按修正意见改一下"这种请求去派它。它内部也是一个完整的 ReAct Agent,有自己的工具链(第三节 3.4 展开)。
Verify Agent —— 只负责校验、不改 HTML。它读 HTML 产物 + Research Agent 用过的原始素材,做"程序化结构校验 + LLM 事实校验"两层检查,产出结构化的修正意见(fix_hint 列表)交给 Host Agent(第五节 5.2 展开)。
三个 Agent 都是 LangGraph 意义上的独立子图,Host Agent 通过 task 工具异步调用另两个。 LangGraph 在物理层面是三层嵌套(外层 Graph / 中层 Host / 内层两个 SubAgent),在逻辑层面就是这个三角分工 。
二、先从一个真实 badcase 说起
在讲两条链路怎么协作之前,先看一个真实出过问题的页面。
某天巡检到一份重疾险深度解读报告在 C 端偶发渲染错位 —— 最后一个"风险提示"卡片下,下一个无关模块被挤歪了。翻开 HTML 的最后几行:
... < div text-card class = "desc" > 重大疾病赔付后合同终止,轻症赔付后该项责任终止... </ div > </ div > </ div > </ div > ← 第 201 行: 多出来的孤儿闭合标签
问题很隐蔽。整份报告的顶层本来是平铺结构(
和各种 card 组件作为兄弟元素并列),没有外层包裹
。但 LLM 凭"印象"在末尾补了一个
当作收尾。这个孤儿闭合标签进到移动端容器,被容器当成关闭自身的信号,导致下一个兄弟组件的位置错乱。
同一时期,另一份惠民保产品的深度解读出了更隐蔽的问题。页面渲染完全正常,视觉上看不出任何毛病,但"特色保障分析"模块里赫然写着:
< div class = "title" highlight-card > 优于市场 < span class = "title-highlight" highlight-card > 85% </ span > 同类惠民保产品 </ div >
"85%"这个数字从哪来的?翻遍 Research Agent 拉到的全部素材 —— 保险条款、投保须知、健康告知 —— 没有任何关于"市场排名"或"百分位"的数据。这是一个典型的 幻觉 :LLM 为了让页面更有说服力,凭空编造了一个具体数字。
如果说孤儿 让页面"塌掉",这个 badcase 让页面"骗人" —— 而且骗得很体面,不翻数据源根本看不出来。
这两个 badcase 恰好对应第一节提到的两类致命错误: 渲染类 (孤儿闭合标签让页面塌掉)和 幻觉类 (无中生有的数据让用户读到假信息)。共同特征: 从文字上看完全"合理",LLM 生成时也没有"犹豫" ,但一个违反了 HTML 结构契约,一个违反了数据事实契约。
对这两类问题,我们要回答两个层次:
这次能不能抓到? — 孤儿
靠纯程序化校验(HTML parser + 闭合规则)就能抓到;"优于市场 85%"则需要拿 HTML 和数据源做对比,靠 LLM 事实校验才能发现。两种校验手段正好对应 Verify Agent 内部的两个节点。
下次能不能不犯? — 这才是关键。每次都能抓到,意味着 离线链路有 verify 环节 可以在刷入 DB 前把问题拦下,不会让 badcase 飘到用户面前。同时,抓到之后,这些错误模式也能回灌到 prompt,让后续生成从源头减少类似错误。
三、多 Agent 是怎么组合起来的
DIPG 工程上是 三层 LangGraph 结构的嵌套 ,这三层正好一对一承载那三个 Agent 的协作。
** 3.1 三层结构 **
外层 用的是 langgraph.graph.StateGraph ,边硬编码。这层的拓扑不依赖 LLM 决策,callback 是必经节点 —— 保证 RPC 回调一定触发。
中层 的 Host Agent 通过 build_domain_agent_v3(blueprint, resources) 构建。Blueprint 是声明式配置:
blueprint = AgentBlueprint( agent_id=DIPG_AGENT_ID, # Host Agent 的唯一标识 instructions=_prompt, # 三阶段闭环 prompt tools=[], # Host 默认加载了 DeepAgents 虚拟文件系统工具 subagents=[ # 挂上另两个 Agent await get_chacha_research_sub_agent(...), # Research Agent await build_chacha_verify_agent(), # Verify Agent ], state_schema=ExtendedAgentState, )
底层基于 LangGraph 的 create_react_agent ,所以 Host Agent 本身就是一个 ReAct 循环 —— LLM 决策是否调工具、调哪个,直到模型主动停止调工具。
"DeepAgent" 是什么? deepagents 是 LangChain 团队维护的开源库( langchain-ai/deepagents )。我在其上做了模型选择、Checkpointer 注入、MCP 工具加载、audit 自动审计记录这些运行时装配。
内层 的 Research Agent 和 Verify Agent 都是 CompiledSubAgent ,各自是独立编译好的 LangGraph 图:
Research Agent 内部也是一个完整的 DeepAgent —— 拥有自己的 ReAct 循环 + 工具链 + 中间件栈
Verify Agent 内部是一个两节点串行的 StateGraph —— structural_check → llm_verify (HelixVerify 的简化版)
** 3.2 SubAgent 是怎么被注入给 Host Agent 的?
—— task 工具 **
LangGraph 里没有"直接调另一个 agent"这种原生操作,所有异构执行必须包装成 工具 。 create_task_tool 做的事:
把 Research Agent 和 Verify Agent 按 name 注册到 agents 字典
创建一个 task(description, subagent_type) 工具
把 task 工具加到 Host Agent 的工具列表
Host Agent 的 LLM 看到的是这样一个 多态工具 :
task ( description : str, subagent_type : str) ├── subagent_type= "chacha_research_agent" : 专业的保险产品研究助手...(即 Research Agent) └── subagent_type= "verify_agent" : 有任何报告验证的工作都交给我...(即 Verify Agent)
这个设计有两个关键后果:
Host Agent 不需要为每个 SubAgent 单独实现调用代码,加新 SubAgent 只需要在 blueprint 里多挂一个
Host Agent 的 LLM 天然用"调工具"的心智模型去编排另两个 Agent —— prompt 里告诉它"阶段 2 把报告交给 Verify Agent",它就知道调 task(description, "verify_agent")
** 3.3 Agent 间事件触发:
task 工具内部做了什么 **
当 Host Agent 的 LLM 决策调 task("研究 prodNo=xxx...", "chacha_research_agent") 时,LangGraph 把调用路由到 handle_langgraph_sub_agent ,它做三件事:
上下文隔离 :每次调用都用新 thread_id + 全新 messages,SubAgent 看不到 Host Agent 的对话历史,也看不到兄弟 SubAgent 之前跑过什么。避免 Verify Agent 被 Research Agent 的"我觉得这段挺好"之类的自述污染。
单一返回值 :Host Agent 只收到一条 ToolMessage ,SubAgent 内部的多轮工具调用、中间推理对它不可见。这保证 Host Agent 的 context 不会被 SubAgent 的细节炸穿。
files 合并 :SubAgent 写的 state["files"] 通过 Command.update 合并回 Host Agent 的 state["files"] 。这是跨 SubAgent 共享数据的主通道。
多 Agent 的时序编排由 Host Agent 的 LLM 自己完成 ,不是代码写死的:
Host Agent 拿到 fix_hint 后 不会再派 Research Agent ,而是自己用 edit_file / write_file 工具直接在 state["files"]["report.html"] 上做局部编辑。
换句话说, 这个 verify-修正闭环不是硬编码的 Graph 边,而是 prompt 层约束下的行为契约 —— 它完全由 Host Agent 按指令驱动,循环多少轮、什么时候停,是 Host Agent 自己根据 Verify Agent 的反馈判断的。
** 3.4 Research Agent 的工具链 + ReAct **
"深度搜索"不是独立的 agent,而是 Research Agent 自己作为 ReAct agent 多轮调工具逐步收集信息。它的工具集只有三类:
sub_agent_tools = [ download_insurance_product_materials, # 按 产品 拉取素材 read_disk_file, # 读具体素材文件 web_search, # 必要时网络检索 ]
通常来说,Research Agent 会尝试:
LLM 读 chacha_prompt 里的 章节,明确要分析的维度
调 download_insurance_product_materials —— 返回素材到磁盘中与素材地图,形如 {"CLAUSE": [...], "INSURE_NOTICE": [...], "HEALTH_INFORM": [...]}
LLM 串联多次 read_disk_file(path) —— 逐个读条款、投保须知、健康告知
条款里缺的数据(如行业发病率),LLM 调 web_search(query) 补充
所有信息收齐后,LLM 进入"页面生成阶段",产出 HTML
这个过程由 LLM 自主决策,没有固定编排。chacha_prompt 控制:
告诉 LLM 素材类型和工具能力
明确"阶段一:工具调用" vs "阶段二:页面生成"不得混用
** 3.5 HTML 产出:写进 state["files"]
作为跨 Agent 记忆 **
Research Agent 的 instructions 末尾被强制追加:
instructions = prompt + ( " \n !!!IMPORTANT: 你交付的 HTML 文件必须是 HTML 组件, 以开头, 以
结尾。" " \n !!!IMPORTANT: 你 必须 (MUST) 调用 write_file 工具将报告正文写入 report.html 文件。" "这是你产出报告的唯一方式。" )
write_file 工具并不直接写物理磁盘 —— 它通过 Command(update={"files": {...}}) 把内容写进 LangGraph state 的 files 字段。 这是一个跨 Agent 共享的虚拟文件系统 。
Verify Agent 启动时的 structural_check 节点调 extract_report_from_files(state) ,从 state["files"]["report.html"] 取 HTML:
async def structural_check ( state: ExtendedAgentState ) -> dict [ str , Any ]: html_content = extract_report_from_files(state) if not html_content: return { "messages" : [AIMessage(content= "...未找到报告文件..." )]} issues = validate_html_string(html_content) ...
Research Agent 和 Verify Agent 不需要通过显式的参数传递来交换 HTML —— state["files"] 作为共享 state,就是它们之间的通道 。
这也解释了前面反复提到的"同一份 /audit/ 数据契约"—— /audit/ 是通过上下文切面自动保存的,属于 files 的子目录:
位置
写入者
读取者
生命周期
state["files"]
LangGraph state(内存中的虚拟文件系统)
write_file 工具 via Command.update
任何能访问 state 的 SubAgent
随 checkpointer 持久化
/audit/
虚拟文件系统 ( workspace/audit_logs/ )
AuditWrapperMiddleware (包装所有工具调用)
Verify Agent 通过 read_file 读取state["files"]["audit"]
随 checkpointer 持久化
files = 生成的 产物 (HTML、中间结果)
/audit/ = 生成的 原料 (工具调用的输入输出)
Verify Agent 用 ls /audit/ + read_file 组合就能读到 Research Agent 期间的全部工具调用记录。
Verify Agent 两者都要看 —— 产物 vs 原料做对齐 ,才能判忠实性。
四、Research Agent 的 prompt 契约:
让它一次过
进入离线 verify 闭环之前,先讲注入给 Research Agent 的 prompt。这部分规则同时服务两件事:
离线链路 : Research Agent 一次过的质量越高,Verify Agent 需要返回的修正意见就越少,verify-修正闭环收敛得更快,计算成本更低。
实时链路 (兜底): 没有 Verify Agent 兜底时,prompt 的约束强度基本等于交付质量的上限。
** 4.1 合规用语硬规则(监管红线) **
- 禁止: "0免赔""零免赔""无免赔" → 合规: "0免赔额""免赔额为0" - 禁止: "100%全赔""多少都能赔" → 合规: "责任内,赔付比例100%" - 禁止: "储蓄险" → 合规: "储蓄型保险" - 禁止: "确诊即赔" → 合规: "首次确诊责任内疾病可赔" ...
监管敏感词, 绝不允许出现 ,硬约束清单。
** 4.2 事实性保证的 8 条规则 **
针对幻觉问题,prompt 里明确了 8 条事实性保证规则(节选):
信源优先级 :产品档案 > 保险条款 > 网络搜索 > 通用知识
无数据不展示 :缺失字段直接隐藏,不做任何臆测
图表真实性 :单点数据不准画趋势图,降级为数字卡片
禁止盲目对比 :没有竞品数据不得使用"优于市场 85%"(第二节那个惠民保 badcase 的直接教训)
否定约束 : is_state_owned: false 就严禁出现"国企/央企"
其中最有意思的是 强制前置溯源 :
利用模型自回归特性,在生成任何关键数据之前,先生成 HTML 注释说明数据来源。格式
数据...
。如果写不出注释,说明该数据是幻觉,必须留空。
不是求 LLM "请标注来源",而是让"写不出来源就不要写数据"变成 自然的生成顺序 。结构强制比语义强制有效得多。
** 4.3 这些规则从哪来? **
简单回答: 一部分是业务合规团队直接给的底线(如合规用语),但相当一部分来自离线链路 Verify Agent 反复抓到的高频错误模式 。这份清单是活的,会随着 Verify Agent 的持续发现而扩充 —— 具体的回灌机制见第六节。
** 4.4 从 prompt 约束到 Verify 兜底的过渡 **
上面的 prompt 契约做得再严,LLM 也 不可能 100% 遵守 。每多一条规则,违规的概率会降低,但不会归零。回到第二节那个惠民保 badcase:"禁止盲目对比"写进 prompt 之后,Research Agent 写"优于市场 85%"的频率会大幅下降,但偶尔仍会出现 —— 惠民保那个 badcase 就是"写了规则仍然犯"的例子。
这正是离线链路需要 Verify Agent 兜底的根本原因: prompt 负责让一次过的概率尽量高,Verify Agent 负责把剩下那些没遵守的抓回来 。两者配合,才有离线链路"每一份刷入 DB 的 HTML 都合格"的交付保证。
下面第五节进入 Verify 闭环本身。
五、Verify Agent 怎么把关交付质量
** 5.1 Host Agent 的三阶段闭环 prompt **
Host Agent 自己也是一个 ReAct Agent, prompt 示例(原有的比较长):
# ### 阶段 1|研究与生成 委派给 chacha_research_agent(Research Agent), 输出 HTML 组件并写入文件系统 # ### 阶段 2|验证循环(核心闭环) ┌─→ 将报告路径 + 数据供给路径(/audit 目录)提交给 verify_agent(Verify Agent) │ ↓ │ Verify Agent 返回验证结论 │ ↓ │ ┌─ 通过 → 退出循环, 进入阶段 3 │ └─ 未通过(返回修正意见) │ ↓ │ 仅按修正意见精准编辑报告, 不做任何额外更改 │ ↓ └──── 重新提交 Verify Agent 循环终止的唯一条件: Verify Agent 不返回任何修正意见 安全上限: 超过 5 轮未通过 → 暂停并汇报分歧点 # ### 阶段 3|交付 输出最终报告路径 + 验证摘要 + 修正记录
** 5.2 Verify Agent 内部:程序化 + LLM **
Verify Agent 由两节点子图组成:
structural_check(程序化校验) —— 纯 Python,基于 html.parser.HTMLParser 自定义的 StructureParser ,检查若干条确定性规则:
规则编号
检查内容
rule1